// javamaps
// Copyright (c) 2010 Lup Gratian
package Rendering.Editor;
import Rendering.IVisual;
import Rendering.*;
import Rendering.Utils.*;
import Core.*;
import Rendering.Info.StreetInfo;
import Rendering.Info.StreetPointInfo;
import Rendering.Renderers.StreetRenderer;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.VolatileImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

public final class LineEditor implements IRenderer {
    // Contine informatii asociate unui punct de pe linie.
    // Punctele fiecarei strazi formeaza o lista dublu-inlantuita.
    public class PointVisual implements IVisual {
        private Street street_;
        private Point position_;
        private int index_;    // Pozitia punctului in lista.
        private PointVisual previous_; // Formeaza o lista inlantuita
        private PointVisual next_;     // cu toate punctele strazii.

        /*
        * Constructori.
        */
        public PointVisual(Street street, Point position, int index) {
            street_ = street;
            position_ = position;
            index_ = index;
        }

        /*
        * Metode publice.
        */
        public Point Position() { return position_; }
        public void SetPosition(Point value) { position_ = value; }
        
        public Street Street() { return street_; }

        public PointVisual Previous() { return previous_; }
        public void SetPrevious(PointVisual value) { previous_ = value; }
        
        public PointVisual Next() { return next_; }
        public void SetNext(PointVisual value) { next_ = value; }

        public int Index() { return index_; }
        public void SetIndex(int value) { index_ = value; }

        public void Draw(Graphics2D g, View view) {
            double x = position_.X() - view.Bounds().Left();
            double y = position_.Y() - view.Bounds().Top();
            double size;

            // Seteaza culoarea punctului.
            if(this == selectedPoint_) {
                // Punctul este selectat.
                g.setColor(SELECTED_POINT_COLOR);
                size = SELECTED_POINT_SIZE;
            }
            else {
                // Punctul face partea din strada selectata.
                if(street_ == selectedStreet_) {
                    g.setColor(STREET_POINT_COLOR);
                    size = STREET_POINT_SIZE;
                }
                else {
                    // Punct obisnuit.
                    g.setColor(POINT_COLOR);
                    size = POINT_SIZE;
                }
            }

            double centerX = x - (size / 2);
            double centerY = y - (size / 2);
            g.fillOval((int)centerX, (int)centerY, (int)size, (int)size);
            g.setColor(Color.BLACK);
            g.drawOval((int)centerX, (int)centerY, (int)size, (int)size);

            // Afiseaza textul (doar pentru punctul/strada selectata).
            if((this == selectedPoint_) || (street_ == selectedStreet_)) {
                FontMetrics metrics = g.getFontMetrics(pointFont_);
                String name = Integer.toString(index_);
                double nameX = x - (metrics.stringWidth(name) / 2) - 1;
                double nameY = y + (metrics.getHeight() / 2);

                g.setColor(FONT_COLOR);
                g.drawString(name, (int)nameX, (int)nameY);
            }
        }

        @Override
        public int hashCode() {
            return street_.hashCode() ^ position_.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            PointVisual other = (PointVisual) obj;
            return (street_.equals(other.street_)) &&
                   (position_.equals(other.position_));
        }
    }

    /*
    * Constante.
    */
    private final double POINT_SIZE = 12;
    private final double STREET_POINT_SIZE = 20;
    private final double SELECTED_POINT_SIZE = 20;
    private final Color POINT_COLOR = new Color(255, 255, 255, 220);    // Alb.
    private final Color STREET_POINT_COLOR = new Color(10, 227, 255);   // Albastru.
    private final Color SELECTED_POINT_COLOR = new Color(125, 222, 51); // Verde.
    private final double FONT_SIZE = 8;
    private final Color FONT_COLOR = Color.BLACK;

    /*
    * Membrii.
    */
    private IMapProvider provider_;
    private IRendererHost host_;
    private StreetRenderer streetRenderer_;
    private IProjection projection_;
    private Region2D maxBounds_;
    private boolean visible_;
    private int zoomLevels_;

    private HashMap<Street, PointVisual> streetStart_; // Legatura intre o strada si primul punct.
    private PointTree<PointVisual> points_;
    private BasicCollection<PointVisual> visiblePoints_;
    private ArrayList<PointVisual> frontPoints_;

    private Font pointFont_;
    private PointVisual selectedPoint_;
    private Street selectedStreet_;
    private boolean mouseCaptured_;
    private Point dragStart_;
    private ArrayList<Point> startPositions_;
    private ArrayList<PointVisual> startVisuals_;

    /*
    * Constructori.
    */
    public LineEditor(StreetRenderer streetRenderer, IRendererHost host) {
        host_ = host;
        streetRenderer_ = streetRenderer;
        provider_ = host.MapProvider();
        projection_ = provider_.Projection();
        zoomLevels_ = provider_.ZoomLevels();
        visible_ = true;

        // Creeaza structurile de date folosite.
        streetStart_ = new HashMap<Street, PointVisual>();
        visiblePoints_ = new BasicCollection<PointVisual>();
        maxBounds_ = provider_.MapBounds(zoomLevels_ - 1);
        points_ = new PointTree<PointVisual>(maxBounds_.Width(), maxBounds_.Height());
        startPositions_ = new ArrayList<Point>();
        startVisuals_ = new ArrayList<PointVisual>();
        frontPoints_ = new ArrayList<PointVisual>();
        pointFont_ = new Font("Dialog", Font.ITALIC, (int)FONT_SIZE);

        LoadPoints();
    }

    /*
     * Metode publice.
     */
    public boolean IsEditor() { return true; }
    public ILayer Layer() { return streetRenderer_.Layer(); }
    public boolean HasPrefetcher() { return false; }
    public IPrefetcher Prefetcher() {
        throw new UnsupportedOperationException("Not supported.");
    }

    public int ZIndex() { return Integer.MAX_VALUE - 2; } // Sub overlay.
    public void SetZIndex(int value) {
        throw new UnsupportedOperationException("Not supported.");
    }

    public boolean MouseDown(Point point, View view, Modifier modifier) {
        if(visible_ == false) return false;

        // Cauta punctul de sub pozitia data.
        PointVisual pointVis = HitTestImpl(point, view);
        if(pointVis != null) {
            // A fost gasit un punct.
            if(modifier.IsSet(Modifier.BUTTON_RIGHT)) {
                // Adauga un punct in vecinatatea celui gasit.
                selectedPoint_ = AddPoint(AdjustPoint(point, view),
                                          pointVis.Street(), pointVis.Index() + 1);
                if(selectedPoint_ == null) {
                    selectedStreet_ = null; // Actiunea nu a fost validata.
                }
                else {
                    // Selecteaza strada asociata.
                    selectedStreet_ = selectedPoint_.Street();
                }
            }
            else {
                selectedPoint_ = pointVis;
                selectedStreet_ = pointVis.Street(); // Selecteaza strada asociata.
            }
            
            mouseCaptured_ = true; // Incepe operatia de drag.
            dragStart_ = point;
            AddStartPositions(selectedPoint_);

            // Notifica parintele si redeseneaza.
            host_.SendAction(Action.ObjectSelected(HitTest(point, view)));
            host_.Repaint();
            return true; // Nu mai trimite evenimentul la alte module.
        }
        else {
            if(modifier.IsSet(Modifier.KEY_SHIFT)) { // Shift apasat.
                // Adauga o noua strada cu punct initial aflat sub cursor.
                // Actiunile vor fi trimise de 'AddStreet'.
                selectedStreet_ = AddStreet(AdjustPoint(point, view));
                if(selectedStreet_ != null) {
                    selectedPoint_ = streetStart_.get(selectedStreet_);
                    mouseCaptured_ = true;
                    dragStart_ = point;
                    AddStartPositions(selectedPoint_);

                    // Notifica parintele si redeseneaza.
                    host_.SendAction(Action.ObjectSelected(HitTest(point, view)));
                    host_.Repaint();
                    return true;
                }
            }

            // Deselecteaza vechiul punct.
            selectedPoint_ = null;
            host_.Repaint();
        }

        // Daca un puct nu a fost gasit, cauta o linie.
        IObjectInfo info = streetRenderer_.HitTest(point, view);
        if(info != null) {
            assert(info.Type() == InfoType.Street);
            selectedStreet_ = ((StreetInfo)info).Street();
            mouseCaptured_ = true;
            dragStart_ = point;
            AddStartPositions(selectedStreet_);

            // Notifica parintele si redeseneaza.
            host_.SendAction(Action.ObjectSelected(info));
            host_.Repaint();
            return true;
        }
        else {
            selectedStreet_ = null;
            host_.Repaint();
        }

        return false; // Trimite evenimentul la alte module.
    }
    
    public boolean MouseUp(Point point, View view, Modifier modifier) {
        if(visible_ == false) return false;
        
        if(mouseCaptured_) {
            if(selectedStreet_ != null) {
                streetRenderer_.UpdateName(selectedStreet_);
            }
            
            mouseCaptured_ = false;
            host_.Repaint();
            return true;
        }

        return false;
    }

    public boolean MouseMoved(Point point, View view, Modifier modifier) { return false; }
    public boolean MouseCaptured() { return mouseCaptured_; }

    public boolean MouseDragged(Point point, View view, Modifier modifier) {
        assert(mouseCaptured_);
        // ------------------------------------------------
        if(visible_ == false) return false;

        if(selectedPoint_ != null) {
            // Un singur punct este selectat.
            MovePoint(selectedPoint_, point,
                      startPositions_.get(0), selectedPoint_.Street());
        }
        else {
            // O intreaga strada este selectata.
            int count = startPositions_.size();
            for(int i = 0; i < count; i++) {
                if(MovePoint(startVisuals_.get(i),point, startPositions_.get(i), selectedStreet_) == false) {
                    break; // Mutarea nu a fost acceptata, abandonam.
                }
            }
        }

        host_.Repaint(); // Forteaza redesenarea.
        return true;
    }

    public IObjectInfo HitTest(Point point, View view) {
        PointVisual pointVis = HitTestImpl(point, view);
        if(pointVis != null) {
            return new StreetPointInfo(pointVis.Street(), this, pointVis.Position(),
                                       ToCoordinates(pointVis.Position()), pointVis.Index());
        }

        return null;
    }

    public double Opacity() { return 1.0; }
    public void SetOpacity(double value) {}

    public boolean Visible() { return visible_; }
    public void SetVisible(boolean value) { visible_ = value; }

    // Sterge liniile si punctele asociata cu pozitia strazii specificat.
    public void RemovePoint(Street street, int index) {
        assert(street != null);
        assert(index >= 0 && index < street.Coordinates().size());
        // ------------------------------------------------
        PointVisual pointVis = streetStart_.get(street);
        if(pointVis == null) return;

        // Gaseste elementul asociat.
        for(int i = 0; i < index; i++) {
            pointVis = pointVis.Next();
        }

        PointVisual prevVis = pointVis.Previous();
        PointVisual nextVis = pointVis.Next();

        // Actualizeaza liniile si legaturile intre puncte.
        if(pointVis.Previous() == null) {
            // Primul punct.
            if(pointVis.Next() == null) {
                // Anunta ca a acesta a fost ultimul punct
                // si strada trebuie stearsa!
                StreetInfo info = new StreetInfo(street, this);
                Action action = Action.ObjectRemoved(info);
                host_.SendAction(action);
                if(action.Valid() == false) return;
            }
            else {
                RemoveLine(pointVis.Position(), nextVis.Position(), street);
                nextVis.SetPrevious(null);
                streetStart_.put(street, nextVis); // Seteaza noul cap al listei.
            }
        }
        else if(pointVis.Next() == null) {
            // Ultimul punct.
            RemoveLine(prevVis.Position(), pointVis.Position(), street);
            prevVis.SetNext(null); // Rupe legatura.
        }
        else {
            // Doua linii trebuie sterse si una adaugata in loc.
            RemoveLine(prevVis.Position(), pointVis.Position(), street);
            RemoveLine(pointVis.Position(), nextVis.Position(), street);
            AddLine(prevVis.Position(), nextVis.Position(), street);

            prevVis.SetNext(nextVis);
            nextVis.SetPrevious(prevVis);
        }

        // Scade 1 din indexul tuturor punctelor aflate dupa acesta.
        while(nextVis != null) {
            nextVis.SetIndex(nextVis.Index() - 1);
            nextVis = nextVis.Next();
        }

        // Sterge punctul din editor si redeseneaza.
        points_.Remove(pointVis);
        host_.Repaint();
    }

    // Sterge toate liniile si punctele asociate cu strada specificata.
    public void RemoveStreet(Street street) {
        assert(street != null);
        // ------------------------------------------------
        List<Coordinates> list = street.Coordinates();
        for(int i = 0; i < list.size(); i++) {
            RemovePoint(street, 0);
        }
    }

    // Actualizeaza numele strazii specificate.
    public void NameChanged(Street street) {
        assert(street != null);
        // ------------------------------------------------
        streetRenderer_.UpdateName(street);
    }

    // Obtine o lista cu punctele aflate in vecinatatea punctului dat.
    public BasicCollection<PointVisual> NearPoints(Point point, double maxDistance) {
        assert(point != null);
        assert(maxDistance >= 0);
        // ------------------------------------------------
        // Obtine toate punctele suficient de apropiate.
        BasicCollection<PointVisual> list = new BasicCollection<PointVisual>();
        points_.Near(point, maxDistance, list);
        return list;
    }

    public void Render(View view) {
        // Nu afisa nimic daca nu suntem la ultimul nivel de zoom.
        if((int)view.Zoom() != (zoomLevels_ - 1)) return;

        // Activeaza antialiasing.
        VolatileImage buffer = view.GetBuffer(this);
        Graphics2D g = buffer.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                           RenderingHints.VALUE_ANTIALIAS_ON);

        // Determina punctele vizibile in regiunea curenta.
        Region2D bounds = view.Bounds();
        double inflateVal = 2 * POINT_SIZE;

        visiblePoints_.Clear();
        bounds.Inflate(inflateVal, inflateVal);
        points_.Intersect(bounds, visiblePoints_);
        bounds.Inflate(-inflateVal, -inflateVal);

        Draw(visiblePoints_, g, view);
        g.dispose();
    }

    /*
     * Metode private.
     */
    // Incarca toate punctele care definesc strazile.
    private void LoadPoints() {
        Iterator<Street> streetIt = provider_.GetStreetIterator();
        while(streetIt.hasNext()) {
            Street street = streetIt.next();

            // Adauga toate punctele ce definesc strada.
            List<Coordinates> coords = street.Coordinates();
            PointVisual previous = null;
            
            int count = coords.size();
            for(int i = 0; i < count; i++) {
                // Transforma coordonatele in pixeli pt. ultimul nivel de zoom.
                Point point = FromCoordinates(coords.get(i));
                PointVisual pointVis = new PointVisual(street, point, i);

                if(i == 0) {
                    // Se leaga primul punct de strada.
                    streetStart_.put(street, pointVis);
                }
                else {
                    // Se leaga punctul precedent de cel curent.
                    previous.SetNext(pointVis);
                    pointVis.SetPrevious(previous);
                }

                points_.Add(pointVis);
                previous = pointVis;
            }
        }
    }

    // Deseneaza punctele care definesc o strada.
    private void Draw(BasicCollection<PointVisual> points,
                      Graphics2D g, View view) {
        if(points.Count() == 0) return;

        // Punctele de pe strada selectata se deseneaza la sfarsit,
        // pentru a fi intotdeauna deasupra celorlalte.
        frontPoints_.clear();

        Iterator<PointVisual> pointIt = points.Objects().iterator();
        while(pointIt.hasNext()) {
            PointVisual pointVis = pointIt.next();
            if(pointVis.Street() == selectedStreet_) {
                frontPoints_.add(pointVis);
            }
            else {
                pointVis.Draw(g, view);
            }
        }

        int count = frontPoints_.size();
        for(int i = 0; i < count; i++) {
            frontPoints_.get(i).Draw(g, view);
        }
    }

    private PointVisual HitTestImpl(Point point, View view) {
        Point test = AdjustPoint(point, view);
        PointTree<PointVisual>.NearestInfo nearest = points_.NearestPoint(test);
        if(nearest == null) return null;

        if(nearest.Distance() < POINT_SIZE) {
            return nearest.Value();
        }

        return null; // Punctul nu este suficient de aproape.
    }

    private Point AdjustPoint(Point point, View view) {
        return new Point(point.X() + view.Bounds().Left(),
                         point.Y() + view.Bounds().Top());
    }

    private void AddStartPositions(Street street) {
        startPositions_.clear();
        startVisuals_.clear();
        PointVisual pointVis = streetStart_.get(street);

        do {
            startPositions_.add(new Point(pointVis.Position()));
            startVisuals_.add(pointVis);
            pointVis = pointVis.Next(); // Treci la urmatorul elem. din lista.
        } while(pointVis != null);
    }

    private void AddStartPositions(PointVisual pointVis) {
        startPositions_.clear();
        startPositions_.add(new Point(pointVis.Position()));
        startVisuals_.add(pointVis);
    }

    private Point FromCoordinates(Coordinates coord) {
        return projection_.FromCoordinates(coord, zoomLevels_ - 1);
    }

    private Coordinates ToCoordinates(Point point) {
        return projection_.ToCoordinates(point, zoomLevels_ - 1);
    }

    private void AddLine(Point a, Point b, Street street) {
        streetRenderer_.AddLine(new Line(a, b, street), zoomLevels_ - 1);
    }

    private void RemoveLine(Point a, Point b, Street street) {
        streetRenderer_.RemoveLine(new Line(a, b, street), zoomLevels_ - 1);
    }

    // Actualizeaza liniile afisate dupa ce un punct al unei strazi
    // a fost mutat. Vechiile linii sunt sterse si unele noi adaugate.
    private void UpdateLines(PointVisual pointVis, Point oldPoint) {
        Street street = pointVis.Street();

        if(pointVis.Previous() == null) {
            // Primul punct; cel mult o linie trebuie stearsa si readaugata.
            if(pointVis.Next() == null) {
                // Un singur punct apartine acestei strazi.
                return;
            }

            RemoveLine(oldPoint, pointVis.Next().Position(), street);
            AddLine(pointVis.Position(), pointVis.Next().Position(), street);
        }
        else if(pointVis.Next() == null) {
            // Ultimul punct; o linie trebuie stearsa si readaugata.
            RemoveLine(pointVis.Previous().Position(), oldPoint, street);
            AddLine(pointVis.Previous().Position(), pointVis.Position(), street);
        }
        else {
            // Doua linii trebuie sterse si readaugate.
            RemoveLine(pointVis.Previous().Position(), oldPoint, street);
            AddLine(pointVis.Previous().Position(), pointVis.Position(), street);

            RemoveLine(oldPoint, pointVis.Next().Position(), street);
            AddLine(pointVis.Position(), pointVis.Next().Position(), street);
        }
    }

    // Un punct ce defineste o strada a fost mutat. Notifica parintele
    // si redeseneaza liniile care au fost afectate.
    private boolean MovePoint(PointVisual pointVis, Point mousePoint,
                              Point start, Street street) {
        // Notifica parintele, iar daca actiunea este valida 
        // sterge punctul din vechea pozitie si adauga-l in cea noua.
        double dx = mousePoint.X() - dragStart_.X();
        double dy = mousePoint.Y() - dragStart_.Y();
        Point oldPos = pointVis.Position();
        Point newPos = new Point(start.X() + dx, start.Y() + dy);

        StreetPointInfo info = new StreetPointInfo(street, this, newPos, 
                                                   ToCoordinates(newPos), pointVis.Index());
        Action action = Action.ObjectMoved(info);
        host_.SendAction(action);
        
        if (action.Valid()) {
            // Actiunea a fost validata; modifica punctul.
            points_.Remove(pointVis);
            pointVis.SetPosition(newPos);
            points_.Add(pointVis);
            
            // Sterge liniile si adauga-le la noile pozitii.
            UpdateLines(pointVis, oldPos);
            return true;
        }

        return false; // Nu s-a putut face mutarea.
    }

    // Adauga un nou punct pe o strada la pozitia specificata.
    // Parintele este notificat si liniile sunt adaugate.
    private PointVisual AddPoint(Point point, Street street, int index) {
        // Creeaza punctul.
        PointVisual pointVis = new PointVisual(street, point, index);
        selectedPoint_ = pointVis;

        // Anunta clientul.
        StreetPointInfo info = new StreetPointInfo(street, this, point,
                                                   ToCoordinates(point), index);
        Action action = Action.ObjectAdded(info);
        host_.SendAction(action);
        if(action.Valid()) {
            points_.Add(pointVis);

            // Gaseste punctul aflat inaintea celui adaugat.
            PointVisual prevVis = streetStart_.get(street);
            for(int i = 0; i < index - 1; i++) {
                prevVis = prevVis.Next();
            }

            // Leaga noul punct de cele existente.
            if(index == 0) {
                // Primul punct al strazii.
                if(prevVis != null) {
                    // Adauta o linie la inceput.
                    AddLine(pointVis.Position(), prevVis.Position(), street);
                    pointVis.SetNext(prevVis);
                    prevVis.SetPrevious(pointVis);
                }

                // Plaseaza punctul in capul listei.
                streetStart_.put(street, pointVis);
            }
            else {
                pointVis.SetNext(prevVis.Next());
                prevVis.SetNext(pointVis);
                pointVis.SetPrevious(prevVis);
                
                if(pointVis.Next() != null) {
                    // Sterge linia curenta dintre Previous si Next
                    // si adauga doua linii care trec prin noul punct.
                    RemoveLine(prevVis.Position(), pointVis.Next().Position(), street);
                    AddLine(prevVis.Position(), pointVis.Position(), street);
                    AddLine(pointVis.Position(), pointVis.Next().Position(), street);
                    pointVis.Next().SetPrevious(pointVis);
                }
                else {
                    // Adauga o linie la sfarsit.
                    AddLine(prevVis.Position(), pointVis.Position(), street);
                }
            }

            // Incrementeaza toate punctele dupa cel inserat.
            PointVisual p = pointVis.Next();
            while(p != null) {
                p.SetIndex(p.Index() + 1);
                p = p.Next();
            }
        }

        return pointVis;
    }

    // Adauga o noua strada care are ca pozitie de start punctul dat.
    private Street AddStreet(Point point) {
        Street street = new Street(ObjectId.NewId(), StreetType.Street);
        StreetInfo info = new StreetInfo(street, this);

        if(host_.SendAction(Action.ObjectAdded(info)).Valid()) {
            // Strada poate fi adaugata. Adauga primul punct.
            AddPoint(point, street, 0);
        }

        return street;
    }
}